/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.service.impl;

import es.silenus.detecta.service.AlfrescoService;
import es.silenus.detecta.model.User;

import org.alfresco.webservice.accesscontrol.*;

import org.alfresco.webservice.content.Content;
import org.alfresco.webservice.content.ContentServiceSoapPort;

import org.alfresco.webservice.repository.UpdateResult;
import org.alfresco.webservice.repository.RepositoryServiceSoapPort;

import org.alfresco.webservice.types.*;

import org.alfresco.webservice.util.*;
import org.alfresco.webservice.administration.NewUserDetails;
import org.alfresco.webservice.administration.AdministrationServiceSoapPort;

import org.apache.commons.io.FileUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.userdetails.UserDetails;

import java.io.File;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;


/**
 * Alfresco upload service implementation.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @since 24-jun-2009 19:19:00
 */
public class AlfrescoServiceImpl implements AlfrescoService, InitializingBean {
	/**
	 * Logger for this class.
	 */
	private static final Log LOG = LogFactory.getLog(AlfrescoServiceImpl.class);

	/**
	 * The store.
	 */
	public static final Store STORE = new Store(Constants.WORKSPACE_STORE, "SpacesStore");

	/**
	 * Organization.
	 */
	public static final String PROP_ORGANIZATION = Constants.createQNameString(Constants.NAMESPACE_CONTENT_MODEL, "organization");

	/**
	 * The endpoint address.
	 */
	private String endpointAddress;

	/**
	 * The user.
	 */
	private String endpointUser;

	/**
	 * The password.
	 */
	private String endpointPassword;

	/**
	 * The workspace URL.
	 */
	private String workspaceURL;

	/**
	 * The access control service.
	 */
	private AccessControlServiceSoapPort accessControlService;

	/**
	 * The administration service.
	 */
	private AdministrationServiceSoapPort administrationService;

	/**
	 * The repository service.
	 */
	private RepositoryServiceSoapPort repositoryService;

	/**
	 * The content service.
	 */
	private ContentServiceSoapPort contentService;

	/**
	 * The organization sub folders.
 	 */
	private List<String> organizationSubFolders;


	/**
	 * Constructor.
	 */
	public AlfrescoServiceImpl() {
		organizationSubFolders = new ArrayList<String>();
	}


	/**
	 * Sets the endpoint address.
	 *
	 * @param endpointAddress the endpoint address.
	 */
	public void setEndpointAddress(final String endpointAddress) {
		this.endpointAddress = endpointAddress;
	}

	/**
	 * Sets the user.
	 *
	 * @param endpointUser the user.
	 */
	public void setEndpointUser(final String endpointUser) {
		this.endpointUser = endpointUser;
	}

	/**
	 * Sets the password.
	 *
	 * @param endpointPassword the endpoint password.
	 */
	public void setEndpointPassword(final String endpointPassword) {
		this.endpointPassword = endpointPassword;
	}

	/**
	 * Sets the workspace url.
	 *
	 * @param workspaceURL the workspace url.
	 */
	public void setWorkspaceURL(final String workspaceURL) {
		this.workspaceURL = workspaceURL;
	}

	/**
	 * Sets the organization sub folders.
	 *
	 * @param organizationSubFolders the organization sub folders.
	 */
	public void setOrganizationSubFolders(final List<String> organizationSubFolders) {
		this.organizationSubFolders = organizationSubFolders;
	}

	/**
	 * Invoked by a BeanFactory after it has set all bean properties supplied (and satisfied BeanFactoryAware and
	 * ApplicationContextAware).<p>This method allows the bean instance to perform initialization only possible
	 * when all bean properties have been set and to throw an exception in the event of misconfiguration.</p>
	 *
	 * @throws Exception in the event of misconfiguration (such as failure to set an essential property) or if
	 * 				 initialization fails.
	 */
	public void afterPropertiesSet() throws Exception {
		WebServiceFactory.setEndpointAddress(endpointAddress);
		accessControlService = WebServiceFactory.getAccessControlService();
		administrationService = WebServiceFactory.getAdministrationService();
		repositoryService = WebServiceFactory.getRepositoryService();
		contentService = WebServiceFactory.getContentService();
	}

	/**
	 * Refresh web service connection.
	 */
	private void refreshSession() {
		try {
			AuthenticationUtils.endSession();
			AuthenticationUtils.startSession(endpointUser, endpointPassword);
		} catch(Throwable e) {
			LOG.error("Error refreshing session", e);
		}
	}

	/**
	 * Parses a path.
	 *
	 * @param folderPath the path.
	 *
	 * @return the converted alfresco path.
	 */
	public Reference makeFolder(final String folderPath) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		StringBuilder pathBuffer = new StringBuilder("/app:company_home");
		Reference folder = null;
		String currentFolder;
		String parentPath;
		String childPath;

		String[] parts = folderPath.split("/");
		boolean createdOne = false;

		for(String part : parts) {
			currentFolder = part.trim();

			if(currentFolder.length() > 0) {
				parentPath = pathBuffer.toString();

				pathBuffer.append("/cm:").append(ISO9075.encode(currentFolder));

				childPath = pathBuffer.toString();

				if(!createdOne) {
					folder = queryNode(childPath);
				} else {
					folder = null;
				}

				if(folder == null) {
					folder = createFolder(parentPath, currentFolder);
					createdOne = true;
				}
			}
		}

		return folder;
	}

	private StringBuilder convertPath(final String path) {
		StringBuilder pathBuffer = new StringBuilder("/app:company_home");
		String[] parts = path.split("/");

		String subpath;

		for(String part : parts) {
			subpath = part.trim();

			if(subpath.length() > 0) {
				pathBuffer.append("/cm:").append(ISO9075.encode(subpath));
			}
		}

		return pathBuffer;
	}

	/**
	 * Retrieves a node reference.
	 *
	 * @param nodePath the node path.
	 *
	 * @return the node reference or null.
	 */
	private Reference getNode(final String nodePath) {
		Reference nodeReference = new Reference(STORE, null, nodePath);

		try {
			Node[] result = repositoryService.get(new Predicate(new Reference[] { nodeReference }, null, null));

			if((result != null) && (result.length == 1)) {
				return result[0].getReference();
			}
		} catch(Throwable e) {
			if(LOG.isDebugEnabled()) {
				LOG.debug("Failed to retrieve node: \"" + nodePath + "\"", e);
			}
		}

		return null;
	}


	/**
	 * Retrieves a node reference.
	 *
	 * @param nodePath the node path.
	 *
	 * @return the node reference or null.
	 */
	private Reference queryNode(final String nodePath) {

		try {
			Query query = new Query(Constants.QUERY_LANG_LUCENE, String.format("PATH:\"%s\"", nodePath));

			Node[] result = repositoryService.get(new Predicate(null, STORE, query));
			if((result != null) && (result.length == 1)) {
				return result[0].getReference();
			}

		} catch (RemoteException e) {
			if(LOG.isDebugEnabled()) {
				LOG.debug("Failed to query node: \"" + nodePath + "\"", e);
			}
		}


		return null;
	}

	/**
	 * Creates a new folder.
	 *
	 * @param parentPath the parent path.
	 * @param folderName the folder name.
	 *
	 * @return the reference or null.
	 */
	private Reference createFolder(final String parentPath, final String folderName) {
		ParentReference parentReference = new ParentReference(
				STORE, null, parentPath, Constants.ASSOC_CONTAINS,
				Constants.createQNameString(Constants.NAMESPACE_CONTENT_MODEL, folderName));

		NamedValue[] properties = new NamedValue[] { Utils.createNamedValue(Constants.PROP_NAME, folderName) };
		CMLCreate create = new CMLCreate("1", parentReference, null, null, null, Constants.TYPE_FOLDER, properties);
		CML cml = new CML();

		cml.setCreate(new CMLCreate[] { create });

		try {
			UpdateResult[] results = repositoryService.update(cml);

			return results[0].getDestination();
		} catch(RemoteException e) {
			LOG.error("Error creating folder: " + folderName + " in path: " + parentPath);
		}

		return null;
	}

	/**
	 * Make directories.
	 *
	 * @param path the full path.
	 *
	 * @return true if successful, false otherwise.
	 */
	public boolean mkdirs(final String path) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		return makeFolder(path) != null;
	}

	/**
	 * Uploads a file to alfresco.
	 *
	 * @param file the file.
	 * @param mimeType the mime type.
	 * @param encoding the encoding.
	 * @param remoteFolderPath the remote file path.
	 *
	 * @return true if upload was successful, false otherwise.
	 */
	@SuppressWarnings({"UnusedAssignment"})
	public boolean uploadFile(File file, final String mimeType, final String encoding, String remoteFolderPath) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		Reference contentNode = null;
		Content content = null;

		Reference folderReference = makeFolder(remoteFolderPath);

		if(folderReference != null) {
			String nodePath = convertPath(remoteFolderPath + "/" + file.getName()).toString();

			contentNode = queryNode(nodePath);

			if(contentNode == null) {
				// First create the content node
				ParentReference parentReference = new ParentReference(
						STORE, null, folderReference.getPath(), Constants.ASSOC_CONTAINS,
						Constants.createQNameString(Constants.NAMESPACE_CONTENT_MODEL, file.getName()));

				NamedValue[] properties = new NamedValue[] { new NamedValue(Constants.PROP_NAME, false, file.getName(), null) };
				CMLCreate create = new CMLCreate(
						"1", parentReference, null, null, Constants.ASSOC_CONTAINS, Constants.TYPE_CONTENT, properties);
				CML cml = new CML();

				cml.setCreate(new CMLCreate[] { create });

				try {
					UpdateResult[] result = repositoryService.update(cml);

					if((result != null) && (result.length == 1)) {
						contentNode = result[0].getDestination();
					}
				} catch(Throwable e) {
					LOG.error("Error creating content: " + file.getName() + " at path: " + remoteFolderPath, e);

					return false;
				}
			}

			// Then write the content
			if(contentNode != null) {
				try {
					byte[] bytes = FileUtils.readFileToByteArray(file);

					content = contentService.write(
							contentNode, Constants.PROP_CONTENT, bytes, new ContentFormat(mimeType, encoding));
				} catch(Throwable e) {
					LOG.error("Error writing content : " + file.getName() + " at path: " + remoteFolderPath, e);

					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Retrieves the organization folder URL.
	 *
	 * @param organizationName the organization name.
	 *
	 * @return the organization folder URL.
	 */
	public String getOrganizationFolderURL(final String organizationName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		String result = workspaceURL;
		Reference folder = queryNode("/app:company_home/cm:" + ISO9075.encode(organizationName));

		if(folder == null) {
			// Check folder
			checkOrganizationFolder(organizationName);
			folder = queryNode("/app:company_home/cm:" + ISO9075.encode(organizationName));
		}

		if(folder != null) {
			result = result + "/" + folder.getUuid();
		}

		return result;
	}

	/**
	 * Checks a organization folder.
	 *
	 * @param organizationName the organization name.
	 *
	 * @return true if successful, false otherwise.
	 */
	public boolean checkOrganizationFolder(final String organizationName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		try {
			final String organizationPath = "/app:company_home/cm:" + ISO9075.encode(organizationName);

			// Retrieve the folder reference
			Reference folderReference = queryNode(organizationPath);

			// If folder does not exist, create it and add the proper permissions
			if(folderReference == null) {
				folderReference = createFolder("/app:company_home", organizationName);

				for(String subFolder: organizationSubFolders) {
					createFolder(organizationPath, subFolder);
				}

				// Set folder permissions
				setFolderPermissions(folderReference, Constants.GROUP_PREFIX + organizationName);
			}

			return true;
		} catch(RemoteException e) {
			LOG.error("Error checking folder of group: " + organizationName, e);

			return false;
		}
	}

	/**
	 * Sets the folder permissions.
	 *
	 * @param folderReference the folder reference.
	 * @param groupName the group or user name.
	 *
	 * @return the ACLs.
	 *
	 * @throws RemoteException if something goes wrong.
	 */
	private ACL[] setFolderPermissions(final Reference folderReference, final String groupName) throws RemoteException {
		Predicate folderPredicate = new Predicate(new Reference[] { folderReference }, null, null);

		// Reset ACEs
		accessControlService.removeACEs(folderPredicate, null);
		// Reset inherit
		accessControlService.setInheritPermission(folderPredicate, false);
		// Set owner
		accessControlService.setOwners(folderPredicate, groupName);

		// Set ACEs
		ACE[] aces = new ACE[] { new ACE(groupName, Constants.FULL_CONTROL, AccessStatus.acepted) };

		return accessControlService.addACEs(folderPredicate, aces);
	}


	/**
	 * Checks if a given group exists.
	 *
	 * @param groupName the group name.
	 * @return false if the group does not exist.
	 */
	public boolean groupExists(final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		AuthorityFilter groupFilter = new AuthorityFilter("GROUP", true);

		try {

			String[] authorities = accessControlService.getAllAuthorities(groupFilter);

			String group = Constants.GROUP_PREFIX + groupName;
			for(String authority: authorities) {
				if(group.endsWith(authority)) {
					return true;
				}
			}


		} catch (RemoteException e) {
			LOG.error("Error retrieving GROUP authorities", e);
		}

		return false;
	}

	/**
	 * Creates a group.
	 *
	 * @param groupName the group name.
	 * @return true if successfully created the group, false otherwise.
	 */
	public boolean createGroup(final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		NewAuthority[] newAuthorities = new NewAuthority[] { new NewAuthority("GROUP", groupName) };

		try {
			String[] created = accessControlService.createAuthorities(null, newAuthorities);

			return created.length == 1;
		} catch(RemoteException e) {
			LOG.error("Error creating group \"" + groupName + "\"", e);
		}

		return false;
	}

	/**
	 * Removes a group.
	 *
	 * @param groupName the group name.
	 * @return true if successfully removed the group, false otherwise.
	 */
	public boolean removeGroup(final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		try {
			String[] authorities = new String[] { Constants.GROUP_PREFIX + groupName };

			accessControlService.deleteAuthorities(authorities);

			return true;
		} catch(RemoteException e) {
			LOG.error("Error removing group \"" + groupName + "\"", e);
		}

		return false;
	}

	/**
	 * Checks if a user exists.
	 *
	 * @param userName the user name.
	 * @return true if the user exists, false otherwise.
	 */
	public boolean userExists(final String userName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}
		try {

			return administrationService.getUser(userName) != null;

		} catch(Throwable e) {
			if(LOG.isDebugEnabled()) {
				LOG.debug("Error retrieving user \"" + userName + "\"", e);
			}

		}

		return false;
	}

	/**
	 * Creates a user.
	 *
	 * @param userDetails the user details.
	 * @return true if created, false otherwise.
	 */
	public boolean createUser(final UserDetails userDetails) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		NewUserDetails newUser = new NewUserDetails();
		NewUserDetails[] newUsers = new NewUserDetails[] { newUser };

		NamedValue[] properties = toProperties(userDetails);

		newUser.setUserName(userDetails.getUsername());
		newUser.setPassword(userDetails.getPassword());
		newUser.setProperties(properties);

		try {
			administrationService.createUsers(newUsers);

			return true;
		} catch(RemoteException e) {
			LOG.error("Error creating user \"" + userDetails.getUsername() + "\"", e);
		}

		return false;
	}

	/**
	 * Updates an existing user.
	 *
	 * @param userDetails the user details.
	 * @return true if updated, false otherwise.
	 */
	public boolean updateUser(final UserDetails userDetails) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		org.alfresco.webservice.administration.UserDetails updated = new org.alfresco.webservice.administration.UserDetails();
		org.alfresco.webservice.administration.UserDetails[] users = { updated };


		NamedValue[] properties = toProperties(userDetails);

		updated.setUserName(userDetails.getUsername());
		updated.setProperties(properties);

		try {
			administrationService.updateUsers(users);

			return true;
		} catch(RemoteException e) {
			LOG.error("Error updating user \"" + userDetails.getUsername() + "\"", e);
		}

		return false;
	}


	/**
	 * Extract properties from the given user details.
	 *
	 * @param userDetails the user details.
	 * @return the properties.
	 */
	private NamedValue[] toProperties(final UserDetails userDetails) {

		final ArrayList<NamedValue> properties = new ArrayList<NamedValue>();

		if(User.class.isAssignableFrom(userDetails.getClass())) {

			final User theUser = (User)userDetails;

			final String[] nameParts = theUser.getFullname().split(" ");
			final StringBuilder lastName = new StringBuilder();
			final String firstName = nameParts[0];

			for(int i = 1; i < nameParts.length; ++i) {
				lastName.append(nameParts[i]).append(' ');
			}

			// Login
			properties.add(Utils.createNamedValue(Constants.PROP_USERNAME, theUser.getName()));
			// First name
			properties.add(Utils.createNamedValue(Constants.PROP_USER_FIRSTNAME, firstName));
			// Last name
			properties.add(Utils.createNamedValue(Constants.PROP_USER_LASTNAME, lastName.toString()));
			// Email
			properties.add(Utils.createNamedValue(Constants.PROP_USER_EMAIL, theUser.getEmail()));
			// Organization
			properties.add(Utils.createNamedValue(PROP_ORGANIZATION, theUser.getOrganization().getName()));
			properties.add(Utils.createNamedValue(Constants.PROP_USER_ORGID, String.valueOf(theUser.getOrganization().getId())));

		} else {

			// Login
			properties.add(Utils.createNamedValue(Constants.PROP_USERNAME, userDetails.getUsername()));
			
		}

		return properties.toArray(new NamedValue[properties.size()]);
	}


	/**
	 * Removes a user from alfresco.
	 *
	 * @param userName the user name.
	 * @return true if removed, false otherwise.
	 */
	public boolean removeUser(final String userName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		try {
			administrationService.deleteUsers(new String[] { userName });

			return true;

		} catch(RemoteException e) {
			LOG.error("Error removing user \"" + userName + "\"", e);
		}

		return false;	
	}

	/**
	 * Checks if a given user belongs to a group or not.
	 *
	 * @param userName the user name.
	 * @param groupName the group name.
	 * @return true if the user belongs to the given group, false otherwise.
	 */
	public boolean hasGroup(final String userName, final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		final String group = Constants.GROUP_PREFIX + groupName;

		SiblingAuthorityFilter filter = new SiblingAuthorityFilter();
		filter.setAuthorityType("GROUP");
		filter.setImmediate(true);

		try {
			String[] groups = accessControlService.getParentAuthorities(userName, filter);
			if(groups != null) {
				for(String userGroup: groups) {
					if(userGroup.equals(group)) {
						return true;
					}
				}
			}
		} catch (RemoteException e) {
			LOG.error(String.format("Error retrieving groups of user \"%s\"", userName), e);
		}

		return false;
	}

	/**
	 * Adds a user to the group.
	 *
	 * @param userName the user name.
	 * @param groupName the group name.
	 * @return true if succesfully added, false otherwise.
	 */
	public boolean addUserToGroup(final String userName, final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		final String group = Constants.GROUP_PREFIX + groupName;

		try {



			String[] created = accessControlService.addChildAuthorities(group, new String[] { userName });

			return created.length == 1;

		} catch(RemoteException e) {
			LOG.error("Error adding user \"" + userName + "\" to group \"" + groupName + "\"", e);
		}

		return false;
	}

	/**
	 * Removes a user from a group.
	 *
	 * @param userName the user name.
	 * @param groupName the group name.
	 * @return true if succesfully added, false otherwise.
	 */
	public boolean removeUserFromGroup(final String userName, final String groupName) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		final String group = Constants.GROUP_PREFIX + groupName;

		try {
			accessControlService.removeChildAuthorities(group, new String[] { userName });

			return true;

		} catch(RemoteException e) {
			LOG.error("Error removing user \"" + userName + "\" to group \"" + groupName + "\"", e);
		}

		return false;
	}


	/**
	 * Change user password.
	 *
	 * @param userName the user.
	 * @param password the new password.
	 * @return the changed password.
	 */
	public boolean changePassword(final String userName, final String password) {
		if(AuthenticationUtils.getTicket() == null) {
			refreshSession();
		}

		try {
			administrationService.changePassword(userName, null, password);

			return true;
		} catch (RemoteException e) {
			LOG.error("Error changing user's \"" + userName + "\" password", e);
		}

		return false;
	}



}
